feat!: report every SSE error response with status, headers, and recoverability#311
Merged
Merged
Conversation
306fef8 to
2343975
Compare
2343975 to
6bcf11f
Compare
joker23
approved these changes
Jun 24, 2026
6bcf11f to
6e64d3e
Compare
…verability Replace UnrecoverableStatusError with SseHttpError, reported on the event stream for any non-200 response -- recoverable or not. It carries the status code, the response headers (which may hold a service directive), and a recoverable flag indicating whether the client will retry on its own (backoff) or has stopped. BREAKING CHANGE: The SSE client now reports recoverable error responses (e.g. 5xx) on the stream; previously only unrecoverable responses surfaced and recoverable ones were retried silently. A consumer that treats any error from the stream as terminal will now tear down on a transient error. Such consumers must check SseHttpError.recoverable and ignore recoverable errors -- the client retries those on its own. UnrecoverableStatusError is removed; use SseHttpError (statusCode, headers, recoverable) instead.
6e64d3e to
21aa063
Compare
kinyoklion
added a commit
that referenced
this pull request
Jun 25, 2026
#312) Stacked on #311 (the `SseHttpError` surfacing this depends on). ## What Honors the FDv1 fallback directive across every way the server can deliver it: - **Successful connection:** the directive is emitted with the basis change set, so the streamed payload is applied *before* the SDK falls back — previously the basis was dropped the moment the header was seen. - **Any error response carrying the header** (`SseHttpError`, recoverable or not): the streaming source closes the connection — which stops the client's own retry — and routes to the fallback tier. - **`goodbye` event with `protocolFallbackTTL`:** treated as an in-band fallback directive, for transports that cannot read response headers. A single helper parses the directive (presence + TTL) from response headers, used for both the successful and error paths; the goodbye path reads its TTL in-band. The streaming source maps `SseHttpError` by its `recoverable` flag: recoverable → interrupted (the client retries), unrecoverable → terminal. The FDv1 streaming source ignores recoverable errors so a transient 5xx no longer shuts it down. When a fallback tier is configured the orchestrator engages it. When none is configured, the SDK stays interrupted and retries FDv2 after the directive's TTL (default 1 hour; a TTL of `0` means remain paused with no retry) rather than halting or reconnecting immediately. Source results carry the fallback TTL. ## Tests - Orchestrator: apply-then-engage from a directive-bearing change set, and the three no-fallback cases (finite TTL retries, absent TTL defers, zero TTL pauses). - `streaming_base`: defer-on-success, the TTL header, the goodbye/TTL directive, and the `SseHttpError` paths (recoverable → interrupted/stays open, unrecoverable → terminal/closes, directive → terminal+TTL regardless of recoverability). `protocol_handler` covers `protocolFallbackTTL` parsing. - v3 contract harness `fdv1-fallback` suite passes end-to-end (816 total, 792 ran, exit 0), with no regression. The contract-test-service capability + `fdv1Fallback` config wiring that exercises this in the harness lives on the e2e branch and will land with the v3 contract-tests PR. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes core FDv2 connection lifecycle, tier selection, and flag delivery ordering when servers send fallback signals; behavior shifts are broad but covered by new unit/contract tests. > > **Overview** > Implements end-to-end handling when the server asks the SDK to fall back from FDv2 to FDv1, including **TTL-aware retry** when no FDv1 tier is configured. > > **Directive parsing and propagation:** Adds shared `readFallbackDirective` for `x-ld-fd-fallback` / `x-ld-fd-fallback-ttl`, threads `fdv1FallbackTtl` through `FDv2SourceResult`, and parses in-band `protocolFallbackTTL` on goodbye events. > > **Streaming / polling behavior:** On a **successful** stream open, the directive is **deferred** and emitted with the next change set so the basis payload is applied before fallback (replacing immediate terminal error on connect). **HTTP errors** with the header, and **goodbye** with TTL or a pending header, surface as terminal fallback results so the orchestrator does not recycle past the directive. Polling mirrors goodbye/header TTL stamping. FDv2 streaming maps `SseHttpError` by recoverability; legacy FDv1 streaming **ignores recoverable** SSE HTTP errors so transient 5xx no longer shuts the source down. > > **Orchestrator:** Classifies directives into engage FDv1 tier, defer retry (no tier: wait TTL—default 1h, zero = pause indefinitely), or none; schedules recycle via `_pendingRetryDelay` and interruptible `_delay` on stop. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 74e7c4b. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
kinyoklion
pushed a commit
that referenced
this pull request
Jun 25, 2026
🤖 I have created a release *beep* *boop* --- ## [3.0.0](launchdarkly_event_source_client-v2.2.0...launchdarkly_event_source_client-v3.0.0) (2026-06-25) ### ⚠ BREAKING CHANGES * report every SSE error response with status, headers, and recoverability ([#311](#311)) ### Features * report every SSE error response with status, headers, and recoverability ([#311](#311)) ([0707b60](0707b60)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > This PR only updates version, changelog, and Release Please manifest; runtime risk depends on the already-merged #311 change, not these files. > > **Overview** > Release Please bumps **`launchdarkly_event_source_client`** from **2.2.0** to **3.0.0** and records the semver-major release in the monorepo manifest and package changelog. > > The **3.0.0** entry documents the breaking behavior already landed in [#311](#311): **every SSE HTTP error response** is now surfaced on the event stream with **status**, **headers**, and whether the failure is **recoverable** (retry vs stop). Consumers that only handled success events or assumed errors were silent must update their stream error handling; the major version signals that contract change even though this PR’s diff is mostly versioning and release notes. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit f467397. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
⚠ Breaking change
The SSE client now reports recoverable error responses (e.g. 5xx) on the stream. Previously only unrecoverable responses surfaced and recoverable ones were retried silently. A consumer that treats any error from the stream as terminal will now tear down on a transient error — it must check
SseHttpError.recoverableand ignore recoverable errors (the client retries those itself).UnrecoverableStatusErroris removed; useSseHttpErrorinstead.This bumps event_source to a major (2.2.0 → 3.0.0).
What
Replaces
UnrecoverableStatusErrorwithSseHttpError, reported on the event stream for any non-200 response — recoverable or not. It carries:statusCodeheaders(always — may hold a service directive)recoverable— whether the client will retry on its own (backoff) or has stoppedWhy
A LaunchDarkly streaming endpoint can deliver the FDv2-to-FDv1 fallback directive (
x-ld-fd-fallback) in the headers of an otherwise-retriable error response (e.g. a 500). Previously recoverable responses surfaced nothing, so that directive was invisible and the client just kept reconnecting. Now every error response is reported with its headers and arecoverableflag, and the consumer decides what to do. This keeps the client a pure transport — it reports what it saw rather than taking an injected retry policy. The browserEventSourcecannot observe responses and reports nothing, as before.Tests
state_connectingcovers a recoverable error (backs off, reportsrecoverable: truewith headers) and an unrecoverable one (goes idle, reportsrecoverable: falsewith headers).First of a two-PR stack; the fallback behavior that consumes this is in the stacked follow-up.
Note
High Risk
Breaking public API and stream semantics: recoverable HTTP failures now emit errors, so existing consumers may tear down connections incorrectly unless they check
SseHttpError.recoverable.Overview
Breaking: Replaces
UnrecoverableStatusErrorwithSseHttpError(statusCode,headers,recoverable). The public export and all call sites move to the new type.Non-200 HTTP responses are now always surfaced on the event stream when the transport can read them. Recoverable statuses (unchanged retry set via
ErrorUtils.isHttpStatusCodeRecoverable) still backoff and reconnect, but the client now callseventSink.addErrorwithSseHttpError(recoverable: true)so consumers can read headers (e.g.x-ld-fd-fallback) on transient failures. Unrecoverable responses still transition to idle and reportrecoverable: false.Stream subscribers that treat any error as fatal must ignore errors where
recoverableis true. Tests assert both paths include directive headers.Reviewed by Cursor Bugbot for commit 4938bed. Bugbot is set up for automated code reviews on this repo. Configure here.